﻿<script setup lang="ts" name="Tinymce">
import type { Editor } from 'tinymce'
import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/silver' // 主题
import 'tinymce/icons/default/icons' // 默认图标
import 'tinymce/models/dom' // 必须引入，否则报错
import 'tinymce/plugins/advlist' // 高级列表
import 'tinymce/plugins/anchor' // 锚点
import 'tinymce/plugins/autolink' // 自动连接
import 'tinymce/plugins/autosave' // 自动保存
import 'tinymce/plugins/charmap' // 特殊字符
import 'tinymce/plugins/code' // 源代码
import 'tinymce/plugins/codesample' // 插入代码示例
import 'tinymce/plugins/directionality' // 文字对齐方向
import 'tinymce/plugins/emoticons' // 表情包
import 'tinymce/plugins/emoticons/js/emojis.js' // 表情包文件
import 'tinymce/plugins/fullscreen' // 全屏
import 'tinymce/plugins/help' // 帮助
import 'tinymce/plugins/image' // 上传图片
import 'tinymce/plugins/importcss' // 引入css 该插件不能放在plugins中
import 'tinymce/plugins/insertdatetime' // 插入时间
import 'tinymce/plugins/link' // 链接
import 'tinymce/plugins/lists' // 列表
// import 'tinymce/plugins/media' // 插入媒体
import 'tinymce/plugins/nonbreaking' // 插入不间断空格
import 'tinymce/plugins/pagebreak' // 插入分页符
import 'tinymce/plugins/preview' // 预览
import 'tinymce/plugins/quickbars' // 快捷工具栏
import 'tinymce/plugins/save' // 保存
import 'tinymce/plugins/searchreplace' // 查找与替换
import 'tinymce/plugins/table' // 表格
import 'tinymce/plugins/template' // 插入模板
import 'tinymce/plugins/visualblocks' // 显示元素范围
import 'tinymce/plugins/visualchars' // 显示不可见字符
import 'tinymce/plugins/wordcount' // 统计字数
import './plugins/formatpainter/plugin.min.js' // 格式刷
import './plugins/indent2em/plugin.min.js' // 首行缩进
import './plugins/letterspacing/plugin.min.js' // 字间距
import './plugins/layout/plugin.min.js' // 一键布局
import './plugins/importword/plugin.min.js' // word文档导入
import './plugins/media/plugin.min.js' // 插入媒体
import 'tinymce/plugins/help/js/i18n/keynav/en.js'
import 'tinymce/plugins/help/js/i18n/keynav/zh_CN.js'
import 'tinymce/plugins/help/js/i18n/keynav/zh_TW.js'

import { unref } from 'vue'
import { toolbarList, pluginsList } from './tinymce'
import { buildShortUUID } from '@/utils/random'
import { bindHandlers } from './helper'
import { onMountedOrActivated } from '@/hooks/web/useOnMountedOrActivated'
import { isNumber } from '@/utils/is'
import { storeToRefs } from 'pinia'
import { FileApi, convertFileUploadField } from '@/api/infra/resource/library/index'
import { useAppStoreWithOut } from '@/store/modules/app'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
import { setSourceBtns } from './sourceBtns'
import { setQuickBtns } from './quickBtns'
import { getAcceptSize, getMimetype, getFileExt, AcceptSizeType } from '@/utils/file/helper'
import { AllowMimetypeType } from '@/utils/file/type'
import { isSuccess } from '@/utils/axios/helper'

const { getPrefixCls } = useDesign()

const prefixCls = getPrefixCls('tinymce')

const appStore = useAppStoreWithOut()
const { systemConfig } = storeToRefs(appStore)
const localeStore = useLocaleStoreWithOut()
const { currentLocale } = storeToRefs(localeStore)
const { t } = useI18n() // 国际化
const { VITE_PUBLIC_PATH } = import.meta.env
const message = useMessage() // 消息弹窗

const props = defineProps({
  options: {
    type: Object,
    default: () => {
      return {}
    }
  },
  value: {
    type: String
  },

  toolbar: {
    type: Array,
    default: toolbarList
  },
  plugins: {
    type: String,
    default: pluginsList
  },
  modelValue: {
    type: String
  },
  height: {
    type: [Number, String],
    required: false,
    default: 400
  },
  width: {
    type: [Number, String],
    required: false,
    default: 'auto'
  },
  showImageUpload: {
    type: Boolean,
    default: true
  },
  useResource: {
    type: Boolean,
    default: false
  }
})
const emits = defineEmits(['change', 'update:modelValue', 'inited', 'init-error'])
// 安全获取当前组件实例
const currentInstance = getCurrentInstance();
// 检查是否为 null 并安全访问 attrs
const attrs = currentInstance?.vnode?.props;
const tinymceId = ref(buildShortUUID('tiny-vue'))
const containerWidth = computed(() => {
  const width = props.width
  if (isNumber(width)) {
    return `${width}px`
  }
  return width
})
const editorRef = ref<Editor | null>(null)
const fullscreen = ref(false)
const elRef = ref<HTMLElement | null>(null)

const ResourcePickerRef = ref(null) // 资源选择弹窗
const quickBtns = ref([]) // 快捷插入按钮
const fileConfig = ref<AcceptSizeType>()

const fileUpload = async (file, callback) => {
  const mimetype: AllowMimetypeType = getMimetype(file.name)
  try {
    const res: any = await FileApi.upload({ name: file.name, file: file, filename: file.name, type: 'richText' })
    if (isSuccess(res)) {
      const result = convertFileUploadField(res)
      switch (mimetype) {
        case 'image':
          callback(result.url, {
            alt: result.name
          })
          break
        case 'video':
          callback(result.url, {
            source2: result.name
          })
          break
        default:
          callback(result.url, {
            text: result.name
          })
          break
      }
    }
  } catch (e) {
    console.log(e)
  }
}
const initOptions = computed((): any => {
  const { height, options, toolbar, plugins } = props
  const publicPath = VITE_PUBLIC_PATH || '/'
  const menu = {
      file: { title: 'File', items: 'newdocument restoredraft | print' },
      edit: { title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' },
      view: { title: 'View', items: 'code | visualaid visualchars visualblocks | preview fullscreen' },
      insert: { title: 'Insert', items: 'image link media template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor | insertdatetime' },
      format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript indent2em codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | removeformat' },
      tools: { title: 'Tools', items: 'wordcount' },
      table: { title: 'Table', items: 'inserttable | cell row column | advtablesort | tableprops deletetable' },
      help: { title: 'Help', items: 'help' }
    }
  const quickbars_insert_toolbar = 'image media resourceselect link numlist bullist quicktable'
  return {
    selector: `#${unref(tinymceId)}`,
    promotion: false,
    height,
    toolbar,
    toolbar_mode: 'sliding',
    menubar: 'file edit view insert format tools table help', // 菜单栏
    menu,
    plugins,
    language_url: currentLocale.value.tinymceLocale === 'en' ? '' : publicPath + 'tinymce/langs/' + currentLocale.value.tinymceLocale + '.js',
    language: currentLocale.value.tinymceLocale,
    branding: false,
    default_link_target: '_blank',
    link_title: false,
    object_resizing: false,
    auto_focus: true,
    skin: 'custom',
    skin_url: publicPath + 'tinymce/skins',
    content_css: publicPath + 'tinymce/skins/content.min.css',
    resize: false,
    min_height: 200,
    font_size_formats: '8px 10px 12px 14px 16px 18px 24px 36px 48px 64px 72px',
    font_family_formats: currentLocale.value.tinymceLocale === 'zh_CN' ? '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino' : 'Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats',
    icons_url: publicPath + 'tinymce/icons/custom/icons.js',
    icons: 'custom',
    file_picker_types: 'file image media',
    statusbar: false,
    quickbars_insert_toolbar,
    convert_urls: false,
    valid_elements: '*[*]', // 防止tinymce删除span标签
    // images_upload_url: '/demo/upimg.php', // 指定一个接受上传文件的后端处理程序地址
    // images_upload_base_path: '/demo', // 后端处理程序地址的根路径
    images_upload_handler: (blobInfo) => new Promise((resolve) => {
      var file = blobInfo.blob() // 转化为易于理解的file对象
      const fileExtension = getFileExt(file.name)
      const mimetype: AllowMimetypeType = getMimetype(file.name)
      if (fileConfig.value && fileConfig.value.config && fileConfig.value.config[mimetype]) {
        const isAcceptFile = fileConfig.value.config[mimetype].accept.split(',').some((ext: string) => {
          if (ext.indexOf(mimetype)) return true
          return !!(fileExtension && ext.indexOf(fileExtension) > -1)
        })
        if (!isAcceptFile) {
          message.error(t('upload', 'accept', [fileConfig.value.config[mimetype].accept, t('upload', mimetype)]))
          return false
        }
        const isAllowSize = fileConfig.value.config[mimetype].size === 0 || file.size < (fileConfig.value.config[mimetype].size as number)
        if (!isAllowSize) {
          message.error(t('upload', 'size', [fileConfig.value.config[mimetype].sizeText]))
          return false
        }
      } else {
        return false
      }
      fileUpload(file, resolve)
    }),
    file_picker_callback: function (callback, _value, _meta) {
      if (fileConfig.value) {
        // 模拟出一个input用于添加本地文件
        var inputElem = document.createElement('input')
        if (inputElem) {
          inputElem.setAttribute('type', 'file')
          inputElem.setAttribute('accept', fileConfig.value.accept)
          inputElem.click()
          inputElem.onchange = function () {
            if (inputElem.files) {
              var file = inputElem.files[0]
              const fileExtension = getFileExt(file.name)
              const mimetype: AllowMimetypeType = getMimetype(file.name)
              if (fileConfig.value && fileConfig.value.config && fileConfig.value.config[mimetype]) {
                const isAcceptFile = fileConfig.value.config[mimetype].accept.split(',').some((ext: string) => {
                  if (ext.indexOf(mimetype)) return true
                  return !!(fileExtension && ext.indexOf(fileExtension) > -1)
                })
                if (!isAcceptFile) {
                  message.error(t('upload', 'accept', [fileConfig.value.config[mimetype].accept, t('upload', mimetype)]))
                  return false
                }
                const isAllowSize = fileConfig.value.config[mimetype].size === 0 || file.size < (fileConfig.value.config[mimetype].size as number)
                if (!isAllowSize) {
                  message.error(t('upload', 'size', [fileConfig.value.config[mimetype].sizeText]))
                  return false
                }
              } else {
                return false
              }
              fileUpload(file, callback)
            }
          }
        }
      }
    },
    ...options,
    setup: (editor: Editor) => {
      editorRef.value = editor
      // 设置快捷选择按钮
      if (quickBtns.value.length > 0) {
        setQuickBtns(quickBtns.value, menu, editor, editorRef.value)
      }

      if (props.useResource) {
        // 设置资源选择按钮
        setSourceBtns(editor, ResourcePickerRef)
      }
      editor.on('init', (e) => {
        console.log(e, 'editor init')
        // e.target.editorCommands.commands.value.fontname = language.value === 'zh' ? () => { return 'Microsoft YaHei' } : () => { return 'verdana' }
        initSetup(e)
      })
    }
  }
})


watch(
  () => attrs?.disabled,
  () => {
    const editor = unref(editorRef)
    if (!editor) {
      return
    }
    editor.mode.set(attrs?.disabled ? 'readonly' : 'design')
  }
)

const destroy = () => {
  if (tinymce !== null) {
    tinymce?.remove?.(unref(initOptions).selector!)
  }
}

onMountedOrActivated(async () => {
  fileConfig.value = await getAcceptSize()
  if (!initOptions.value?.inline) {
    tinymceId.value = buildShortUUID('tiny-vue')
  }
  nextTick(() => {
    setTimeout(() => {
      initEditor()
    }, 30)
  })
}, destroy)

watch(
  () => currentLocale.value.tinymceLocale,
  () => {
    destroy()
    nextTick(() => {
      setTimeout(() => {
        initEditor()
      }, 30)
    })
  }
)
watch(
  () => systemConfig.value.isLight,
  () => {
    destroy()
    nextTick(() => {
      setTimeout(() => {
        initEditor()
      }, 30)
    })
  }
)
watch(
  () => systemConfig.value.theme.dark.color,
  () => {
    destroy()
    nextTick(() => {
      setTimeout(() => {
        initEditor()
      }, 30)
    })
  }
)

const initSetup = (e) => {
  const editor = unref(editorRef)
  if (!editor) {
    return
  }
  const value = props.modelValue || ''
  editor.setContent(value)
  bindModelHandlers(editor)
  bindHandlers(e, attrs, unref(editorRef))
}

const initEditor = () => {
  const el = unref(elRef)
  if (el) {
    el.style.visibility = ''
  }
  tinymce
    .init(unref(initOptions))
    .then((editor) => {
      emits('inited', editor)
    })
    .catch((err) => {
      emits('init-error', err)
    })
}

const setValue = (editor, val, prevVal?: string) => {
  if (
    editor &&
    typeof val === 'string' &&
    val !== prevVal &&
    val !== editor.getContent({ format: attrs?.outputFormat })
  ) {
    editor.setContent(val)
  }
}

const bindModelHandlers = (editor) => {
  const modelEvents = attrs?.modelEvents ? attrs?.modelEvents : null
  const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents

  watch(
    () => props.modelValue,
    (val, prevVal) => {
      setValue(editor, val, prevVal)
    }
  )

  watch(
    () => props.value,
    (val, prevVal) => {
      setValue(editor, val, prevVal)
    },
    {
      immediate: true
    }
  )

  editor.on(normalizedEvents || 'change keyup undo redo', () => {
    const content = editor.getContent({ format: attrs?.outputFormat })
    emits('update:modelValue', content)
    emits('change', content)
  })

  editor.on('FullscreenStateChanged', (e) => {
    fullscreen.value = e.state
  })
}
// 资源选择确定
const resourceSelectConfirm = (data) => {
  let reshtml = ''
  const imgtype = ['jpg', 'jpeg', 'gif', 'png']
  data.forEach(item => {
    if (imgtype.indexOf(item.type) > -1) {
      reshtml += '<img src=' + item.url + ' alt=' + item.alt + ' />'
    } else if (item.type === 'mp4') {
      reshtml += '<video src=' + item.url + '  poster=' + item.attachment?.url + ' controls ><source src=' + item.url + ' /></video>'
    } else if (item.type === 'mp3') {
      reshtml += '<audio src=' + item.url + ' controls ><source src=' + item.url + '  /></audio>'
    }
  })
  if (editorRef.value) {
    if (editorRef.value.selection.getContent()) {
      editorRef.value.selection.setContent(reshtml)
    } else {
      editorRef.value.insertContent(reshtml)
    }
  }
}
</script>

<template>
  <div>
    <Teleport to="body" :disabled="!fullscreen">
      <div :class="prefixCls" :style="{ width: containerWidth }">
        <textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }" />
        <ResourcePicker ref="ResourcePickerRef" @save="resourceSelectConfirm" />
      </div>
    </Teleport>
  </div>
</template>

<style lang="scss" scoped>
.#{$namespace}-tinymce {
  position: relative;
  z-index: 9999;
  line-height: normal;
}

textarea {
  z-index: -1;
  visibility: hidden;
}
</style>
